<?php
/**
 * @author		João Batista Neto
 * @brief		Classes e interfaces utilitárias
 * @package		rpo.util
 */

require_once 'rpo/base/Object.php';
require_once 'rpo/util/MapReduce.php';

/**
 * @brief	Implementação simples de MapReduce
 * @class	AbstractMapReduce
 */
abstract class AbstractMapReduce extends Object implements MapReduce {
	/**
	 * @brief	Objeto Iterator que contém os dados mapeados
	 * @var		ArrayIterator
	 */
	protected $mapped;

	/**
	 * @brief	Objeto Iterator que contém os dados reduzidos
	 * @var		ArrayIterator
	 */
	protected $reduced;

	/**
	 * @brief	Executa o passo de mapeamento
	 * @param	$map MapStep Objeto que fará o mapeamento
	 */
	abstract protected function doMap( MapStep $map  );

	/**
	 * @brief	Executa o passo de mapeamento
	 * @param	$reduce ReduceStep Objeto que fará o mapeamento
	 */
	abstract protected function doReduce( ReduceStep $reduce );

	/**
	 * @brief	Efetua o mapeamento de um par chave-valor.
	 * @param	$key mixed Chave do mapeamento
	 * @param	$value mixed Valor do mapeamento
	 */
	public function map( $key , $value ){
		if ( !$this->mapped->offsetExists( $key ) ){
			$this->mapped[ $key ] = new ArrayIterator();
		}

		$this->mapped[ $key ]->append( $value );
	}

	/**
	 * @brief	Efetua a redução de um valor relacionado com uma chave.
	 * @param	$key mixed Chave do mapeamento
	 * @param	$value mixed Valor reduzido
	 * @throws	LogicException Se houver a tentativa de chamar o método reduce para uma única
	 * chave mais do que uma vez.
	 * @throws	UnexpectedValueException Se houver a tentativa de se reduzir uma chave que não
	 * foi mapeada pelo método map()
	 */
	public function reduce( $key , $value ){
		if ( $this->mapped->offsetExists( $key ) ){
			if ( $this->reduced->offsetExists( $key ) ){
				throw new LogicException( 'O método reduce() pode ser chamado apenas 1 vez para cada chave.' );
			} else {
				$this->reduced->offsetSet( $key , $value );
			}
		} else {
			throw new UnexpectedValueException( sprintf( 'A chave %s não existe ou não foi mapeada.' , $key ) );
		}
	}

	/**
	 * @brief	Efetua uma operação de mapeamento e redução em todos os itens.
	 * @param	$map MapStep Objeto de mapeamento
	 * @param	$reduce ReduceStep Objeto de redução
	 * @return	ObjectIterator
	 * @see		MapReduce.emit()
	 * @throws	LogicException Se o método MapReduce::map() não for chamado ao menos uma vez
	 * @throws	RuntimeException Se alguma etapa falhar
	 */
	public function mapReduce( MapStep $map , ReduceStep $reduce ){
		$this->mapped = new ArrayIterator();
		$this->reduced = new ArrayIterator();

		try {
			$this->doMap( $map );

			if ( $this->mapped->count() >= 1 ){
				$this->doReduce( $reduce );

				if ( $this->reduced->count() >= 1 ){
					$this->reduced->rewind();
					return $this->reduced;
				} else {
					$this->mapped->rewind();
					return $this->mapped;
				}
			} else {
				throw new LogicException( 'O método MapReduce::map() deve ser chamado ao menos 1 vez.' );
			}
		} catch ( Exception $e ){
			throw new RuntimeException( 'Não foi possível finalizar o processo de mapeamento e redução' , null , $e );
		}
	}
}